﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;

namespace SimpleRest.Serialization
{
    public class XmlSerializer
    {
        /// <summary>
        /// TODO: determine if there is a better way to serialize a list as it comes in.  Create deserialization for this mechanism as well.
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        public string Serialize(object obj)
        {
            if (obj == null)
                return string.Empty;

            var type = TypeCache.GetType(obj);

            if (type.IsCollection && !type.IsPrimitive)
            {
                return this.SerializeCollection(obj);
            }
            else
                return this.Serialize(obj, null);
        }

        private string GetXmlString(string key, string value)
        {
            StringBuilder sb = new StringBuilder();

            sb.Append("<").Append(key);
            if (value == null)
                sb.Append(" />");
            else
            {
                if (value.Contains('<') || value.Contains('>'))
                    sb.Append("><![CDATA[").Append(value).Append("]]></").Append(key).Append(">");
                else
                    sb.Append(">").Append(value).Append("</").Append(key).Append(">");
            }

            sb.Append("\n");
            return sb.ToString();
        }

        private string Serialize(object obj, string parent = null)
        {
            StringBuilder sb = new StringBuilder();
            Type rootType = TypeCache.GetType(obj).Target;
            string rootXmlString = "";

            string rootName = rootType.Name;
            if (parent != null)
                rootName = parent;

            Attributes.XmlNameAttribute rootNameAttribute = TypeCache.GetAttribute<Attributes.XmlNameAttribute>(rootType);
            if (rootNameAttribute != null)
                rootName = rootNameAttribute.Name;

            if (obj == null)
            {
                sb.Append(GetXmlString(rootName, null));
            }
            else
            {
                rootXmlString = "<" + rootName + ">\n";

                if (TypeCache.IsPrimiviteType(rootType))
                {
                    sb.Append(obj.ToString());
                }
                else
                {
                    foreach (var property in TypeCache.GetProperties(rootType))
                    {
                        object propertyValue = property.GetValue(obj, null);
                        Attributes.XmlElementAttribute propertyXmlAttribute = null;
                        string propertyName = property.Name;

                        var attributes = property.GetCustomAttributes(true);
                        foreach (var attribute in attributes)
                        {
                            if (attribute is Attributes.XmlElementAttribute)
                            {
                                propertyXmlAttribute = attribute as Attributes.XmlElementAttribute;
                            }
                        }

                        if (propertyXmlAttribute != null && !string.IsNullOrEmpty(propertyXmlAttribute.Name))
                        {
                            propertyName = propertyXmlAttribute.Name;
                            foreach (char x in propertyName)
                                if (!char.IsLetterOrDigit(x))
                                    throw new Exception("The property name may not contain illegal xml characters.");
                        }

                        if (TypeCache.IsPrimiviteType(property.PropertyType))
                        {
                            if (propertyXmlAttribute == null || !propertyXmlAttribute.IsXmlAttribute)
                            {
                                if (propertyValue != null)
                                {
                                    // sb.Append("<").Append(propertyName).Append(">").Append(propertyValue.ToString()).Append("</").Append(propertyName).Append(">\n");
                                    sb.Append(GetXmlString(propertyName, propertyValue.ToString()));
                                }
                                else
                                {
                                    //sb.Append("<").Append(propertyName).Append(" />\n");
                                    sb.Append(GetXmlString(propertyName, null));
                                }
                            }
                            else
                            {
                                rootXmlString = rootXmlString.Replace(">", " " + propertyName + "=\"" + (propertyValue == null ? "" : propertyValue.ToString()) + "\">");
                            }
                        }
                        else
                        {
                            if (propertyXmlAttribute != null)
                            {
                                if (propertyXmlAttribute.IsXmlAttribute)
                                    throw new Exception("A complex object may not be represented as an xml attribute.");
                            }

                            if (propertyValue == null)
                            {
                                // sb.Append("<").Append(property.Name).Append(" />").Append("\n");
                                sb.Append(GetXmlString(property.Name, null));
                            }
                            else
                            {
                                if (TypeCache.IsCollectionType(property.PropertyType) && !TypeCache.IsPrimiviteType(property.PropertyType))
                                {
                                    var argumentType = TypeCache.GetGenericArgumentForCollection(property.PropertyType);
                                    sb.Append(this.SerializeCollection(propertyValue, property)).Append("\n");
                                }
                                else
                                    sb.Append(this.Serialize(propertyValue, property.Name)).Append("\n");
                            }
                        }

                    }
                }


                sb.Append("</").Append(rootName).Append(">");
            }

            return rootXmlString + sb.ToString();
        }

        private string SerializeCollection(object obj, System.Reflection.PropertyInfo associatedPropertyInfo = null)
        {
            StringBuilder sb = new StringBuilder();
            Type targetType = TypeCache.GetType(obj).Target;
            var nameAttribute = TypeCache.GetAttribute<SimpleRest.Serialization.Attributes.XmlNameAttribute>(targetType);
            Type argumentType = TypeCache.GetGenericArgumentForCollection(targetType);

            string rootName = "";
            if (associatedPropertyInfo != null && nameAttribute == null)
                rootName = associatedPropertyInfo.Name;
            else
                rootName = nameAttribute == null ? "ArrayOf" + argumentType.Name : nameAttribute.Name;

            sb.Append("<").Append(rootName).Append(">").Append("\n");
            System.Collections.IEnumerable items = obj as System.Collections.IEnumerable;
            foreach (var itemObj in items)
            {
                var type = TypeCache.GetType(itemObj);
                if (type != null && !type.IsPrimitive)
                    sb.Append(this.Serialize(itemObj, type.Target.Name)).Append("\n");
                else
                    sb.Append(this.GetXmlString(argumentType.Name, itemObj == null ? null : itemObj.ToString()));
            }
            sb.Append("</").Append(rootName).Append(">");
            return sb.ToString();
        }

        public T DeSerialize<T>(string content)
        {
            return (T)Convert.ChangeType(this.DeSerialize(content, typeof(T)), typeof(T));
        }
        public object DeSerialize(string content, Type type)
        {
            if (TypeCache.IsPrimiviteType(type))
                return Convert.ChangeType(content, type);

            object obj = System.Activator.CreateInstance(type);
            System.Xml.XmlDocument doc = new System.Xml.XmlDocument();
            doc.LoadXml(content);

            return this.DeSerialize(obj, doc.FirstChild);
        }
        private object DeSerialize(object obj, XmlNode inputNode)
        {
            Type type = TypeCache.GetType(obj).Target;

            foreach (XmlNode node in inputNode.ChildNodes)
            {
                Type genericArgument = TypeCache.GetGenericArgumentForCollection(type);
                if (TypeCache.IsCollectionType(type) && TypeCache.IsPrimiviteType(genericArgument))
                {
                    var add = type.GetMethod("Add");
                    if (genericArgument.IsEnum)
                        add.Invoke(obj, new[] { Convert.ChangeType(Enum.Parse(genericArgument, node.InnerText), genericArgument) });
                    else if (genericArgument.FullName == typeof(Guid).FullName)
                    {
                        object value = new Guid(node.InnerText);
                        add.Invoke(obj, new[] { value });
                    }
                    else
                        add.Invoke(obj, new[] { Convert.ChangeType(node.InnerText, genericArgument) });
                    continue;
                }

                foreach (var attribute in node.Attributes)
                {
                }

                var property = TypeCache.GetTypeProperty(type, node.Name);// GetProperty(type, node.Name);
                if (property != null)
                {
                    if (TypeCache.IsPrimiviteType(property.PropertyType))
                    {
                        object value = null;
                        if (property.PropertyType.IsValueType && node.InnerText == null)
                            value = System.Activator.CreateInstance(property.PropertyType);
                        else if (property.PropertyType == typeof(Guid))
                        {
                            value = new Guid(node.InnerText);
                        }
                        else
                        {
                            if (property.PropertyType.IsEnum)
                            {
                                value = Convert.ChangeType(Enum.Parse(property.PropertyType, node.InnerText), property.PropertyType);
                            }
                            else
                                value = Convert.ChangeType(node.InnerText, property.PropertyType);
                        }

                        property.SetValue(obj, value, null);
                    }
                    else
                    {
                        if (TypeCache.IsPrimiviteType(property.PropertyType))
                        {
                            // var listType = typeof(List<>);
                            //var constructedListType = listType.MakeGenericType(property.PropertyType);
                            //var array = Activator.CreateInstance(constructedListType);
                            var array = Activator.CreateInstance(property.PropertyType, new object[] { node.ChildNodes.Count });

                            int i = 0;
                            foreach (XmlNode subNode in node.ChildNodes)
                            {
                                object newItem = System.Activator.CreateInstance(TypeCache.GetGenericArgumentForCollection(property.PropertyType));
                                this.DeSerialize(newItem, subNode);
                                var add = property.PropertyType.GetMethod("Add");
                                add.Invoke(array, new[] { newItem });
                                //SetValue(array, newItem, new object[] { i });
                                // property.GetSetMethod().Invoke(
                                i++;
                            }
                            property.SetValue(obj, array, null);
                        }
                        else
                        {
                            object newItem = System.Activator.CreateInstance(property.PropertyType);
                            var x = this.DeSerialize(newItem, node);
                            property.SetValue(obj, newItem, null);
                        }
                    }
                }
            }

            return obj;
        }
    }
}
